# Copyright 2020 by Kurt Rathjen. All Rights Reserved.
#
# This library is free software: you can redistribute it and/or modify it 
# under the terms of the GNU Lesser General Public License as published by 
# the Free Software Foundation, either version 3 of the License, or 
# (at your option) any later version. This library is distributed in the 
# hope that it will be useful, but WITHOUT ANY WARRANTY; without even the 
# implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. 
# See the GNU Lesser General Public License for more details.
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
"""
Example:

import FXmutils

attr = FXmutils.Attribute("sphere1", "translateX")
attr.set(100)
"""
import logging
from FXstudiovendor import six

try:
	import maya.cmds
except ImportError:
	import traceback
	traceback.print_exc()


logger = logging.getLogger(__name__)


VALID_CONNECTIONS = [
	"animCurve",
	"animBlend",
	"pairBlend",
	"character"
]

VALID_BLEND_ATTRIBUTES = [
	"int",
	"long",
	"float",
	"short",
	"double",
	"doubleAngle",
	"doubleLinear",
]

VALID_ATTRIBUTE_TYPES = [
	"int",
	"long",
	"enum",
	"bool",
	"string",
	"float",
	"short",
	"double",
	"doubleAngle",
	"doubleLinear",
]


class AttributeError(Exception):
	"""Base class for exceptions in this module."""
	pass


class Attribute(object):


	@classmethod
	def listAttr(cls, name, **kwargs):
		"""
		Return a list of Attribute from the given name and matching criteria.

		If no flags are specified all attributes are listed.

		:rtype: list[Attribute]
		"""
		attrs = maya.cmds.listAttr(name, **kwargs) or []
		return [cls(name, attr) for attr in attrs]

	@classmethod
	def deleteProxyAttrs(cls, name):
		"""Delete all the proxy attributes for the given object name."""
		attrs = cls.listAttr(name, unlocked=True, keyable=True) or []
		for attr in attrs:
			if attr.isProxy():
				attr.delete()

	def __init__(self, name, attr=None, value=None, type=None, cache=True):
		"""
		:type name: str
		:type attr: str | None
		:type type: str | None
		:type value: object | None
		:type cache: bool
		"""
		if "." in name:
			name, attr = name.split(".")

		if attr is None:
			msg = "Cannot initialise attribute instance without a given attr."
			raise AttributeError(msg)

		self._name = six.text_type(name)
		self._attr = six.text_type(attr)
		self._type = type
		self._value = value
		self._cache = cache
		self._fullname = None

	def __str__(self):
		"""
		:rtype: str
		"""
		return str(self.toDict())

	def name(self):
		"""
		Return the Maya object name for the attribute.

		:rtype: str
		"""
		return self._name

	def attr(self):
		"""
		Return the attribute name.

		:rtype: str
		"""
		return self._attr

	def isLocked(self):
		"""
		Return true if the attribute is locked.

		:rtype: bool
		"""
		return maya.cmds.getAttr(self.fullname(), lock=True)

	def isUnlocked(self):
		"""
		Return true if the attribute is unlocked.

		:rtype: bool
		"""
		return not self.isLocked()

	def toDict(self):
		"""
		Return a dictionary of the attribute object.

		:rtype: dict
		"""
		result = {
			"type": self.type(),
			"value": self.value(),
			"fullname": self.fullname(),
		}
		return result

	def isValid(self):
		"""
		Return true if the attribute type is valid.

		:rtype: bool
		"""
		return self.type() in VALID_ATTRIBUTE_TYPES

	def isProxy(self):
		"""
	   Return true if the attribute is a proxy

	   :rtype: bool
	   """
		if not maya.cmds.addAttr(self.fullname(), query=True, exists=True):
			return False

		return maya.cmds.addAttr(self.fullname(), query=True, usedAsProxy=True)

	def delete(self):
		"""Delete the attribute"""
		maya.cmds.deleteAttr(self.fullname())

	def exists(self):
		"""
		Return true if the object and attribute exists in the scene.

		:rtype: bool
		"""
		return maya.cmds.objExists(self.fullname())

	def prettyPrint(self):
		"""
		Print the command for setting the attribute value
		"""
		msg = 'maya.cmds.setAttr("{0}", {1})'
		msg = msg.format(self.fullname(), self.value())

	def clearCache(self):
		"""
		Clear all cached values
		"""
		self._type = None
		self._value = None

	def update(self):
		"""
		This method will be deprecated.
		"""
		self.clearCache()

	def query(self, **kwargs):
		"""
		Convenience method for Maya's attribute query command

		:rtype: object
		"""
		return maya.cmds.attributeQuery(self.attr(), node=self.name(), **kwargs)

	def listConnections(self, **kwargs):
		"""
		Convenience method for Maya's list connections command

		:rtype: list[str]
		"""
		return maya.cmds.listConnections(self.fullname(), **kwargs)

	def sourceConnection(self, **kwargs):
		"""
		Return the source connection for this attribute.

		:rtype: str | None
		"""
		try:
			return self.listConnections(destination=False, **kwargs)[0]
		except IndexError:
			return None

	def fullname(self):
		"""
		Return the name with the attr name.

		:rtype: str
		"""
		if self._fullname is None:
			self._fullname = '{0}.{1}'.format(self.name(), self.attr())
		return self._fullname

	def value(self):
		"""
		Return the value of the attribute.

		:rtype: float | str | list
		"""
		if self._value is None or not self._cache:

			try:
				self._value = maya.cmds.getAttr(self.fullname())
			except Exception:
				msg = 'Cannot GET attribute VALUE for "{0}"'
				msg = msg.format(self.fullname())
				logger.exception(msg)

		return self._value

	def type(self):
		"""
		Return the type of data currently in the attribute.

		:rtype: str
		"""
		if self._type is None:

			try:
				self._type = maya.cmds.getAttr(self.fullname(), type=True)
				self._type = six.text_type(self._type)
			except Exception:
				msg = 'Cannot GET attribute TYPE for "{0}"'
				msg = msg.format(self.fullname())
				logger.exception(msg)

		return self._type

	def set(self, value, blend=100, key=False, clamp=True, additive=False):
		"""
		Set the value for the attribute.

		:type key: bool
		:type clamp: bool
		:type value: float | str | list
		:type blend: float
		"""
		try:
			if additive and self.type() != 'bool':
				if self.attr().startswith('scale'):
					value = self.value() * (1 + (value - 1) * (blend/100.0))
				else:
					value = self.value() + value * (blend/100.0)
			elif int(blend) == 0:
				value = self.value()
			else:
				_value = (value - self.value()) * (blend/100.00)
				value = self.value() + _value
		except TypeError as error:
			msg = 'Cannot BLEND or ADD attribute {0}: Error: {1}'
			msg = msg.format(self.fullname(), error)
			logger.debug(msg)

		try:
			if self.type() in ["string"]:
				maya.cmds.setAttr(self.fullname(), value, type=self.type())
			elif self.type() in ["list", "matrix"]:
				maya.cmds.setAttr(self.fullname(), *value, type=self.type())
			else:
				maya.cmds.setAttr(self.fullname(), value, clamp=clamp)
		except (ValueError, RuntimeError) as error:
			msg = "Cannot SET attribute {0}: Error: {1}"
			msg = msg.format(self.fullname(), error)
			logger.debug(msg)

		try:
			if key:
				self.setKeyframe(value=value)
		except TypeError as error:
			msg = 'Cannot KEY attribute {0}: Error: {1}'
			msg = msg.format(self.fullname(), error)
			logger.debug(msg)

	def setKeyframe(self, value, respectKeyable=True, **kwargs):
		"""
		Set a keyframe with the given value.

		:value: object
		:respectKeyable: bool
		:rtype: None
		"""
		if self.query(minExists=True):
			minimum = self.query(minimum=True)[0]
			if value < minimum:
				value = minimum

		if self.query(maxExists=True):
			maximum = self.query(maximum=True)[0]
			if value > maximum:
				value = maximum

		kwargs.setdefault("value", value)
		kwargs.setdefault("respectKeyable", respectKeyable)

		maya.cmds.setKeyframe(self.fullname(), **kwargs)

	def setStaticKeyframe(self, value, time, option):
		"""
		Set a static keyframe at the given time.

		:type value: object
		:type time: (int, int)
		:type option: PasteOption
		"""
		if option == "replaceCompletely":
			maya.cmds.cutKey(self.fullname())
			self.set(value, key=False)

		# This should be changed to only look for animation.
		# Also will need to support animation layers ect...
		elif self.isConnected():

			# TODO: Should also support static attrs when there is animation.
			if option == "replace":
				maya.cmds.cutKey(self.fullname(), time=time)
				self.insertStaticKeyframe(value, time)

			elif option == "replace":
				self.insertStaticKeyframe(value, time)

		else:
			self.set(value, key=False)

	def insertStaticKeyframe(self, value, time):
		"""
		Insert a static keyframe at the given time with the given value.

		:type value: float | str
		:type time: (int, int)
		:rtype: None
		"""
		startTime, endTime = time
		duration = endTime - startTime
		try:
			# Offset all keyframes from the start position.
			maya.cmds.keyframe(self.fullname(), relative=True, time=(startTime, 1000000), timeChange=duration)

			# Set a key at the given start and end time
			self.setKeyframe(value, time=(startTime, startTime), ott='step')
			self.setKeyframe(value, time=(endTime, endTime), itt='flat', ott='flat')

			# Set the tangent for the next keyframe to flat
			nextFrame = maya.cmds.findKeyframe(self.fullname(), time=(endTime, endTime), which='next')
			maya.cmds.keyTangent(self.fullname(), time=(nextFrame, nextFrame), itt='flat')
		except TypeError as error:
			msg = "Cannot insert static key frame for attribute {0}: Error: {1}"
			msg = msg.format(self.fullname(), error)
			logger.debug(msg)

	def setAnimCurve(self, curve, time, option, source=None, connect=False):
		"""
		Set/Paste the give animation curve to this attribute.

		:type curve: str
		:type option: PasteOption
		:type time: (int, int)
		:type source: (int, int)
		:type connect: bool
		:rtype: None
		"""
		fullname = self.fullname()
		startTime, endTime = time

		if self.isProxy():
			logger.debug("Cannot set anim curve for proxy attribute")
			return

		if not self.exists():
			logger.debug("Attr does not exists")
			return

		if self.isLocked():
			logger.debug("Cannot set anim curve when the attr locked")
			return

		if source is None:
			first = maya.cmds.findKeyframe(curve, which='first')
			last = maya.cmds.findKeyframe(curve, which='last')
			source = (first, last)

		# We run the copy key command twice to check we have a valid curve.
		# It needs to run before the cutKey command, otherwise if it fails
		# we have cut keys for no reason!
		success = maya.cmds.copyKey(curve, time=source)
		if not success:
			msg = "Cannot copy keys from the anim curve {0}"
			msg = msg.format(curve)
			logger.debug(msg)
			return

		if option == "replace":
			maya.cmds.cutKey(fullname, time=time)
		else:
			time = (startTime, startTime)

		try:
			# Update the clip board with the give animation curve
			maya.cmds.copyKey(curve, time=source)

			# Note: If the attribute is connected to referenced
			# animation then the following command will not work.
			maya.cmds.pasteKey(fullname, option=option, time=time, connect=connect)

			if option == "replaceCompletely":

				# The pasteKey cmd doesn't support all anim attributes
				# so we need to manually set theses.
				dstCurve = self.animCurve()
				if dstCurve:
					curveColor = maya.cmds.getAttr(curve + ".curveColor")
					preInfinity = maya.cmds.getAttr(curve + ".preInfinity")
					postInfinity = maya.cmds.getAttr(curve + ".postInfinity")
					useCurveColor = maya.cmds.getAttr(curve + ".useCurveColor")

					maya.cmds.setAttr(dstCurve + ".preInfinity", preInfinity)
					maya.cmds.setAttr(dstCurve + ".postInfinity", postInfinity)
					maya.cmds.setAttr(dstCurve + ".curveColor", *curveColor[0])
					maya.cmds.setAttr(dstCurve + ".useCurveColor", useCurveColor)

		except RuntimeError:
			msg = 'Cannot paste anim curve "{0}" to attribute "{1}"'
			msg = msg.format(curve, fullname)
			logger.exception(msg)

	def animCurve(self):
		"""
		Return the connected animation curve.

		:rtype: str | None
		"""
		result = None

		if self.exists():

			n = self.listConnections(plugs=True, destination=False)

			if n and "animCurve" in maya.cmds.nodeType(n):
				result = n

			elif n and "character" in maya.cmds.nodeType(n):
				n = maya.cmds.listConnections(n, plugs=True,
											  destination=False)
				if n and "animCurve" in maya.cmds.nodeType(n):
					result = n

			if result:
				return result[0].split(".")[0]

	def isConnected(self, ignoreConnections=None):
		"""
		Return true if the attribute is connected.

		:type ignoreConnections: list[str]
		:rtype: bool
		"""
		if ignoreConnections is None:
			ignoreConnections = []
		try:
			connection = self.listConnections(destination=False)
		except ValueError:
			return False

		if connection:
			if ignoreConnections:
				connectionType = maya.cmds.nodeType(connection)
				for ignoreType in ignoreConnections:
					if connectionType.startswith(ignoreType):
						return False
			return True
		else:
			return False

	def isBlendable(self):
		"""
		Return true if the attribute can be blended.

		:rtype: bool
		"""
		return self.type() in VALID_BLEND_ATTRIBUTES

	def isSettable(self, validConnections=None):
		"""
		Return true if the attribute can be set.

		:type validConnections: list[str]
		:rtype: bool
		"""
		if validConnections is None:
			validConnections = VALID_CONNECTIONS

		if not self.exists():
			return False

		if not maya.cmds.listAttr(self.fullname(), unlocked=True, keyable=True, multi=True, scalar=True):
			return False

		connection = self.listConnections(destination=False)
		if connection:
			connectionType = maya.cmds.nodeType(connection)
			for validType in validConnections:
				if connectionType.startswith(validType):
					return True
			return False
		else:
			return True
